package com.jprocms.module.cms.permission;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.jpro.framework.common.enums.UserTypeEnum;
import com.jpro.framework.common.util.collection.CollectionUtils;
import com.jpro.framework.common.util.json.JsonUtils;
import com.jpro.framework.datapermission.core.rule.DataPermissionRule;
import com.jpro.framework.mybatis.core.dataobject.BaseDO;
import com.jpro.framework.mybatis.core.util.MyBatisUtils;
import com.jpro.framework.security.core.LoginUser;
import com.jpro.framework.security.core.util.SecurityFrameworkUtils;
import com.jpro.module.system.api.permission.PermissionApi;
import com.jpro.module.system.api.permission.dto.SitePermissionRespDTO;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 站群的 {@link DataPermissionRule} 数据权限规则实现
 * <p>
 * 注意，使用 SitePermissionRule 时，需要保证表中有 site_id 栏目编号的字段，可自定义。
 * <p>
 * 1. 一般情况下，site_id 不进行修改，则会导致用户看到之前的数据。
 * 2. 部分情况下，希望该用户还是能看到之前的数据，则有两种方式解决：【需要你改造该 SitePermissionRule 的实现代码】
 * 1）编写洗数据的脚本，将 site_id 修改成新栏目的编号；【建议】
 * 最终过滤条件是 WHERE site_id = ?
 *
 * @author 泽睿
 */
@AllArgsConstructor
@Slf4j
public class SitePermissionRule implements DataPermissionRule {

    /**
     * LoginUser 的 Context 缓存 Key
     */
    protected static final String CONTEXT_KEY = SitePermissionRule.class.getSimpleName();

    public static final String SITE_COLUMN_NAME = "site_id";

    static final Expression EXPRESSION_NULL = new NullValue();

    private final PermissionApi permissionApi;
    /**
     * 基于栏目的表字段配置
     * 一般情况下，每个表的站点编号字段是 site_id，通过该配置自定义。
     * <p>
     * key：表名
     * value：字段名
     */
    private final Map<String, String> siteColumns = new HashMap<>();


    /**
     * 所有表名，是 {@link #siteColumns}
     */
    private final Set<String> TABLE_NAMES = new HashSet<>();

    @Override
    public Set<String> getTableNames() {
        return TABLE_NAMES;
    }

    @Override
    public Expression getExpression(String tableName, Alias tableAlias) {
        // 只有有登陆用户的情况下，才进行数据权限的处理
        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
        if (loginUser == null) {
            return null;
        }
        // 只有管理员类型的用户，才进行数据权限的处理
        if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) {
            return null;
        }

        // 获得数据权限
        SitePermissionRespDTO sitePermission = loginUser.getContext(CONTEXT_KEY, SitePermissionRespDTO.class);
        // 从上下文中拿不到，则调用逻辑进行获取
        if (sitePermission == null) {
            sitePermission = permissionApi.getSitePermission(loginUser.getId()).getCheckedData();
            if (sitePermission == null) {
                log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser));
                throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限",
                        loginUser.getId(), tableName, tableAlias.getName()));
            }
            // 添加到上下文中，避免重复计算
            loginUser.setContext(CONTEXT_KEY, sitePermission);
        }

        // 情况一，如果是 ALL 可查看全部，则无需拼接条件
        if (sitePermission.getAll()) {
            return null;
        }

        // 情况二，即不能查看 则说明 100% 无权限
        if (CollUtil.isEmpty(sitePermission.getSiteIds())) {
            return new EqualsTo(null, null); // WHERE null = null，可以保证返回的数据为空
        }

        // 情况三，拼接 site 的条件
        return buildSiteExpression(tableName, tableAlias, sitePermission.getSiteIds());
    }

    private Expression buildSiteExpression(String tableName, Alias tableAlias, Set<Long> siteIds) {
        // 如果不存在配置，则无需作为条件
        String columnName = siteColumns.get(tableName);
        if (StrUtil.isEmpty(columnName)) {
            return null;
        }
        // 如果为空，则无条件
        if (CollUtil.isEmpty(siteIds)) {
            return null;
        }
        // 拼接条件
        return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
                new Parenthesis(new ExpressionList(CollectionUtils.convertList(siteIds, LongValue::new))));
    }

    // ==================== 添加配置 ====================

    public void addSiteColumn(Class<? extends BaseDO> entityClass) {
        addSiteColumn(entityClass, SITE_COLUMN_NAME);
    }

    public void addSiteColumn(Class<? extends BaseDO> entityClass, String columnName) {
        String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName();
        addSiteColumn(tableName, columnName);
    }

    public void addSiteColumn(String tableName, String columnName) {
        siteColumns.put(tableName, columnName);
        TABLE_NAMES.add(tableName);
    }

}
